Projet réduction de dimensionnalité et techniques de clustering - Projet 2¶

Chargement des données¶

In [1]:
import pandas as pd
import numpy as np
df_decathlon= pd.read_csv('src/decathlon.csv', sep=';')
df_decathlon.head()
Out[1]:
Unnamed: 0 100m Longueur Poids Hauteur 400m 110m H Disque Perche Javelot 1500m Classement Points Competition
0 Sebrle 10.85 7.84 16.36 2.12 48.36 14.05 48.72 5.0 70.52 280.01 1 8893 JO
1 Clay 10.44 7.96 15.23 2.06 49.19 14.13 50.11 4.9 69.71 282.00 2 8820 JO
2 Karpov 10.50 7.81 15.93 2.09 46.81 13.97 51.65 4.6 55.54 278.11 3 8725 JO
3 Macey 10.89 7.47 15.73 2.15 48.97 14.56 48.34 4.4 58.46 265.42 4 8414 JO
4 Warners 10.62 7.74 14.48 1.97 47.97 14.01 43.73 4.9 55.39 278.05 5 8343 JO
In [2]:
df_decathlon.rename(columns={'Unnamed: 0':'Nom'}, inplace=True)
df_decathlon.head()
Out[2]:
Nom 100m Longueur Poids Hauteur 400m 110m H Disque Perche Javelot 1500m Classement Points Competition
0 Sebrle 10.85 7.84 16.36 2.12 48.36 14.05 48.72 5.0 70.52 280.01 1 8893 JO
1 Clay 10.44 7.96 15.23 2.06 49.19 14.13 50.11 4.9 69.71 282.00 2 8820 JO
2 Karpov 10.50 7.81 15.93 2.09 46.81 13.97 51.65 4.6 55.54 278.11 3 8725 JO
3 Macey 10.89 7.47 15.73 2.15 48.97 14.56 48.34 4.4 58.46 265.42 4 8414 JO
4 Warners 10.62 7.74 14.48 1.97 47.97 14.01 43.73 4.9 55.39 278.05 5 8343 JO
In [3]:
df_decathlon.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 41 entries, 0 to 40
Data columns (total 14 columns):
 #   Column       Non-Null Count  Dtype  
---  ------       --------------  -----  
 0   Nom          41 non-null     object 
 1   100m         41 non-null     float64
 2   Longueur     41 non-null     float64
 3   Poids        41 non-null     float64
 4   Hauteur      41 non-null     float64
 5   400m         41 non-null     float64
 6   110m H       41 non-null     float64
 7   Disque       41 non-null     float64
 8   Perche       41 non-null     float64
 9   Javelot      41 non-null     float64
 10  1500m        41 non-null     float64
 11  Classement   41 non-null     int64  
 12  Points       41 non-null     int64  
 13  Competition  41 non-null     object 
dtypes: float64(10), int64(2), object(2)
memory usage: 4.6+ KB

Pas de données manquantes

In [4]:
df_decathlon.describe()
Out[4]:
100m Longueur Poids Hauteur 400m 110m H Disque Perche Javelot 1500m Classement Points
count 41.000000 41.000000 41.000000 41.000000 41.000000 41.000000 41.000000 41.000000 41.000000 41.000000 41.000000 41.000000
mean 10.998049 7.260000 14.477073 1.976829 49.616341 14.605854 44.325610 4.762439 58.316585 279.024878 12.121951 8005.365854
std 0.263023 0.316402 0.824428 0.088951 1.153451 0.471789 3.377845 0.278000 4.826820 11.673247 7.918949 342.385145
min 10.440000 6.610000 12.680000 1.850000 46.810000 13.970000 37.920000 4.200000 50.310000 262.100000 1.000000 7313.000000
25% 10.850000 7.030000 13.880000 1.920000 48.930000 14.210000 41.900000 4.500000 55.270000 271.020000 6.000000 7802.000000
50% 10.980000 7.300000 14.570000 1.950000 49.400000 14.480000 44.410000 4.800000 58.360000 278.050000 11.000000 8021.000000
75% 11.140000 7.480000 14.970000 2.040000 50.300000 14.980000 46.070000 4.920000 60.890000 285.100000 18.000000 8122.000000
max 11.640000 7.960000 16.360000 2.150000 53.200000 15.670000 51.650000 5.400000 70.520000 317.000000 28.000000 8893.000000

Pas de données aberrantes à première vue.

Corrélation entre variables¶

In [5]:
corr_matrice = df_decathlon.corr(numeric_only=True)
corr_matrice
Out[5]:
100m Longueur Poids Hauteur 400m 110m H Disque Perche Javelot 1500m Classement Points
100m 1.000000 -0.598678 -0.356482 -0.246253 0.520298 0.579889 -0.221708 -0.082537 -0.157746 -0.060546 0.296704 -0.684272
Longueur -0.598678 1.000000 0.183304 0.294644 -0.602063 -0.505410 0.194310 0.204014 0.119759 -0.033686 -0.604055 0.725135
Poids -0.356482 0.183304 1.000000 0.489212 -0.138433 -0.251616 0.615768 0.061182 0.374956 0.115803 -0.369970 0.627389
Hauteur -0.246253 0.294644 0.489212 1.000000 -0.187957 -0.283289 0.369218 -0.156181 0.171880 -0.044903 -0.492769 0.576703
400m 0.520298 -0.602063 -0.138433 -0.187957 1.000000 0.547988 -0.117879 -0.079292 0.004232 0.408106 0.562119 -0.666940
110m H 0.579889 -0.505410 -0.251616 -0.283289 0.547988 1.000000 -0.326201 -0.002704 0.008743 0.037540 0.439102 -0.644460
Disque -0.221708 0.194310 0.615768 0.369218 -0.117879 -0.326201 1.000000 -0.150072 0.157890 0.258175 -0.389125 0.484183
Perche -0.082537 0.204014 0.061182 -0.156181 -0.079292 -0.002704 -0.150072 1.000000 -0.030001 0.247448 -0.320380 0.197436
Javelot -0.157746 0.119759 0.374956 0.171880 0.004232 0.008743 0.157890 -0.030001 1.000000 -0.180393 -0.208095 0.422393
1500m -0.060546 -0.033686 0.115803 -0.044903 0.408106 0.037540 0.258175 0.247448 -0.180393 1.000000 0.089898 -0.194349
Classement 0.296704 -0.604055 -0.369970 -0.492769 0.562119 0.439102 -0.389125 -0.320380 -0.208095 0.089898 1.000000 -0.739183
Points -0.684272 0.725135 0.627389 0.576703 -0.666940 -0.644460 0.484183 0.197436 0.422393 -0.194349 -0.739183 1.000000
In [6]:
import matplotlib.pyplot as plt
import seaborn as sns
# Changement de police des graphiques
plt.rc('font', family = 'serif', serif = 'cmr10')
plt.rcParams.update({"text.usetex": True, "axes.formatter.use_mathtext" : True})

sns.heatmap(corr_matrice, annot=True, fmt=".1f", cmap='RdBu', vmin=-1, vmax=1, center=0, linewidth=0.5)
plt.title('Corrélations entre les performances dans les différentes disciplines')
plt.show()

Compte-tenu du nombre de variables, il est difficile de donner une interprétation globale de ce diagramme. On peut tout de même remarquer :

  • Une corrélation positive mais moyenne entre les performances dans les disciplines du poids et du disque
  • Même chose pour le 100m et le 110m haies
  • Une corrélation négative et moyenne entre les performances à la longueur et au 100m ou entre le 400 m et la longueur
  • Une bonne performance dans les disciplines telles que la longueur, le poids et la hauteur est corrélée positivement avec le nombre de points. C'est le contraire pour les épreuves de vitesse (100m, 110m haies, 400m).

1. ACP¶

In [7]:
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.pipeline import make_pipeline

scaler = StandardScaler()
pca = PCA()
pipe = make_pipeline(scaler, pca)
In [8]:
features = df_decathlon.iloc[:,1:11]
features_trans = pipe.fit_transform(features)
features_trans = pd.DataFrame(features_trans, columns=['PC'+str(i) for i in range(1,11)])
features_trans.head()
Out[8]:
PC1 PC2 PC3 PC4 PC5 PC6 PC7 PC8 PC9 PC10
0 4.038449 1.365826 -0.289957 1.941134 -0.376955 0.067786 -0.554977 -0.752596 0.062225 0.633131
1 3.919365 0.836961 0.231175 1.493972 1.037609 -0.812650 -0.867515 -0.302845 -0.013215 -0.818729
2 4.619987 0.039995 -0.041586 -1.313526 -0.187730 0.741611 -0.454143 1.070842 -0.180315 0.124575
3 2.233461 1.041766 -1.864362 -0.743214 -0.977270 -0.040017 -0.192708 0.689743 0.438425 -0.166834
4 2.168396 -1.803200 0.851017 -0.284600 0.151395 -0.079388 0.061003 0.214545 0.167392 0.082692

Variance expliquée par chaque PC¶

In [9]:
caract_pc = pd.DataFrame({'Composante':['PC'+str(i) for i in range(1,11)], 'Valeur propre' : np.round(pca.explained_variance_,2),
                         '% Variance' : np.round(pca.explained_variance_ratio_*100,2), '% Variance cumulée' : np.round(np.cumsum(pca.explained_variance_ratio_*100),2)})

caract_pc.set_index('Composante')
Out[9]:
Valeur propre % Variance % Variance cumulée
Composante
PC1 3.35 32.72 32.72
PC2 1.78 17.37 50.09
PC3 1.44 14.05 64.14
PC4 1.08 10.57 74.71
PC5 0.70 6.85 81.56
PC6 0.61 5.99 87.55
PC7 0.46 4.51 92.06
PC8 0.41 3.97 96.03
PC9 0.22 2.15 98.18
PC10 0.19 1.82 100.00

Choix du nombre de composantes¶

Méthode du coude¶

In [10]:
fig, ax = plt.subplots(figsize=(5,5))

ax.bar(np.arange(1,11,1), caract_pc['% Variance'], color='tab:blue')
ax.set_xticks(np.arange(1,11,1), caract_pc['Composante'])
ax.tick_params(axis='y', color='tab:blue', labelcolor='tab:blue')
plt.title("Pourcentage de variance expliquée par chaque composante principale")
ax.set_xlabel("Composante principale")
ax.set_ylabel("Pourcentage de variance expliquée", color='tab:blue')
#Deuxième axe pour les pourcentages cumulés
ax2 = ax.twinx()
ax2.set_ylabel('Pourcentages cumulés', color='tab:red')
ax2.tick_params(axis='y', color='tab:red', labelcolor='tab:red')
ax2.set_ylim(0,100)
ax2.plot(np.arange(1,11,1), caract_pc['% Variance cumulée'], color='tab:red', marker='x')

plt.show()

Il n'y a pas de coude "franc" visuellement parlant. On constate un grand écart de variance expliquée entre 1 et 2 composantes principales, mais ne retenir que deux composantes ne permet d'expliquer que 50 % de la variance.

On constate un autre écart entre 4 et 5 composantes mais moins important. Pour une utilisation ultérieure, il semble que 5 composantes principales soit un bon choix. Cela permet une explication de 82 % de la variance.

Test des bâtons brisés¶

In [11]:
bb = 1/np.arange(10,0,-1)   #Calcul des fraction 1/k pour k allant de 10 à 1 (ordre descendant)
bb = np.cumsum(bb)   #calcul des sommes cumulées des 1/k
bb = bb[::-1]   #inversion de la liste des valeurs pour obtenir les bâtons brisés dans le bon ordre
bb
Out[11]:
array([2.92896825, 1.92896825, 1.42896825, 1.09563492, 0.84563492,
       0.64563492, 0.47896825, 0.33611111, 0.21111111, 0.1       ])
In [12]:
test_bb = pd.DataFrame({'PC':np.arange(1,11), 'Val propre' : pca.explained_variance_, 'batons':bb})
test_bb
Out[12]:
PC Val propre batons
0 1 3.353703 2.928968
1 2 1.780559 1.928968
2 3 1.440040 1.428968
3 4 1.083272 1.095635
4 5 0.701893 0.845635
5 6 0.614250 0.645635
6 7 0.462516 0.478968
7 8 0.406799 0.336111
8 9 0.220185 0.211111
9 10 0.186783 0.100000
In [13]:
fig, ax = plt.subplots(figsize=(5,5))

plt.plot(test_bb['PC'], test_bb['Val propre'], label="Valeur propre", color='tab:blue')
plt.plot(test_bb['PC'], test_bb['batons'], color='tab:red', label="Seuil des bâtons brisés", linestyle="--")
plt.xticks(test_bb['PC'], ['PC'+str(i) for i in range(1,11)])
plt.title("Test des bâtons brisés")
plt.xlabel("Composante principale")
plt.ylabel("Valeur propre")
plt.legend(bbox_to_anchor=(1,1))
plt.show()

Le test suggère de ne garder que la première composante principale (la valeur prore de la deuxième composante est inférieure au seuil pour cette composante), qui ne représente que 33% de la variance. Cela paraît peu. De plus, la courbe des seuils de bâtons brisés suit approximativement la courbe des valeurs propres (qui repasse même au-dessus à partir de PC8).

Règle de Kaiser-Guttman¶

L'ACP a été réalisée sur des valeurs normalisées. La somme des valeurs propres est égale au nombre de variables et leurs moyenne est 1. Le seuil de Kaiser-Guttman est donc 1.

In [14]:
fig, ax = plt.subplots(figsize=(5,5))

ax.bar(np.arange(1,11,1), caract_pc['Valeur propre'], color='tab:blue')
ax.axhline(y=1, xmin=0, xmax=10, linestyle='--', color='crimson')
ax.text(6,1.1,'Seuil de Kaiser-guttman', color='crimson')
ax.set_xticks(np.arange(1,11,1), caract_pc['Composante'])
plt.title("Variance sur chaque composante principale")
ax.set_xlabel("Composante principale")
ax.set_ylabel("Variance")


plt.show()

Cette règle suggère de garder 4 composantes principales.

Règle de Karlis-Saporta-Spinaki¶

La règle de Kaiser-Guttman ne tient pas compte des dimensions des données. Elle peut être trop permissive dans certains cas. La règle de Karlis-Saporta-Spinaki surélève le seuil de Kaiser-Guttman pour tenir compte de la dimension des données :

$Seuil = 1+2\sqrt{\dfrac{p-1}{n-1}}$ où $p$ est le nombre de variables et $n$ le nombre d'observations.

Ici, Le seuil vaut : $1+2\times \sqrt{\dfrac{9}{40}}\approx 1,95$.

In [15]:
fig, ax = plt.subplots(figsize=(5,5))

ax.bar(np.arange(1,11,1), caract_pc['Valeur propre'], color='tab:blue')
ax.axhline(y=1.95, xmin=0, xmax=10, linestyle='--', color='crimson')
ax.text(5,2,'Seuil de Karlis-Saporta-Spinaki', color='crimson')
ax.set_xticks(np.arange(1,11,1), caract_pc['Composante'])
plt.title("Variance sur chaque composante principale")
ax.set_xlabel("Composante principale")
ax.set_ylabel("Variance")


plt.show()

Cette règle suggère de ne garder qu'une composante principale, comme le test des bâtons brisés.

Cercle de corrélations selon PC1 et PC2¶

In [16]:
# Calcul des saturations des variables sur les PC
saturations = pd.DataFrame(pca.components_.T*np.sqrt(pca.explained_variance_),
                           columns=['PC'+str(i) for i in range(1,11)],
                           index=df_decathlon.columns[1:11])
saturations
Out[16]:
PC1 PC2 PC3 PC4 PC5 PC6 PC7 PC8 PC9 PC10
100m -0.784344 0.189467 -0.186698 -0.038288 -0.305951 0.232048 -0.259640 -0.294413 0.049156 0.183368
Longueur 0.751116 -0.349712 0.184475 0.103050 -0.037134 -0.239923 -0.426885 0.013401 0.226487 0.035026
Poids 0.630236 0.605736 -0.023669 0.192959 -0.112532 0.239412 0.210640 0.200227 0.200501 0.168675
Hauteur 0.579050 0.354645 -0.262736 -0.137279 -0.562340 -0.366612 0.062194 -0.079784 -0.114335 -0.045996
400m -0.688053 0.576512 0.133103 0.029666 0.088781 -0.260611 0.084617 -0.136107 0.259081 -0.178922
110m H -0.755516 0.231636 -0.093788 0.294444 -0.166362 -0.078090 -0.243015 0.453554 -0.070449 -0.039270
Disque 0.559330 0.613846 0.043486 -0.262897 0.106129 0.352192 -0.292362 -0.024485 -0.072642 -0.194122
Perche 0.050967 -0.182597 0.700350 0.558386 -0.334058 0.205077 0.066621 -0.113554 -0.038936 -0.119478
Javelot 0.280553 0.320927 -0.394496 0.721126 0.308919 -0.127909 -0.072596 -0.188882 -0.116055 0.037934
1500m -0.058799 0.480115 0.791859 -0.163090 0.155470 -0.233766 -0.056875 -0.008749 -0.144401 0.185507
In [17]:
# saturations de classement et points sur les composantes PC1 et PC2
scaler = StandardScaler()
resultats_stand = pd.DataFrame(scaler.fit_transform(df_decathlon[['Classement','Points']]), columns=['Classement','Points'])
sat_class_pc1 = np.corrcoef(features_trans.iloc[:,0], df_decathlon['Classement'])[0,1]
sat_class_pc2 = np.corrcoef(features_trans.iloc[:,1], df_decathlon['Classement'])[0,1]
sat_class_pc3 = np.corrcoef(features_trans.iloc[:,2], df_decathlon['Classement'])[0,1]
sat_class_pc4 = np.corrcoef(features_trans.iloc[:,3], df_decathlon['Classement'])[0,1]
sat_pts_pc1 = np.corrcoef(features_trans.iloc[:,0], df_decathlon['Points'])[0,1]
sat_pts_pc2 = np.corrcoef(features_trans.iloc[:,1], df_decathlon['Points'])[0,1]
sat_pts_pc3 = np.corrcoef(features_trans.iloc[:,2], df_decathlon['Points'])[0,1]
sat_pts_pc4 = np.corrcoef(features_trans.iloc[:,3], df_decathlon['Points'])[0,1]
In [18]:
# Calcul des cos2
cos2 = saturations**2
cos2
Out[18]:
PC1 PC2 PC3 PC4 PC5 PC6 PC7 PC8 PC9 PC10
100m 0.615196 0.035898 0.034856 0.001466 0.093606 0.053846 0.067413 0.086679 0.002416 0.033624
Longueur 0.564176 0.122299 0.034031 0.010619 0.001379 0.057563 0.182231 0.000180 0.051296 0.001227
Poids 0.397197 0.366916 0.000560 0.037233 0.012663 0.057318 0.044369 0.040091 0.040201 0.028451
Hauteur 0.335299 0.125773 0.069030 0.018845 0.316226 0.134404 0.003868 0.006366 0.013072 0.002116
400m 0.473416 0.332366 0.017716 0.000880 0.007882 0.067918 0.007160 0.018525 0.067123 0.032013
110m H 0.570804 0.053655 0.008796 0.086697 0.027676 0.006098 0.059056 0.205711 0.004963 0.001542
Disque 0.312850 0.376806 0.001891 0.069115 0.011263 0.124039 0.085475 0.000600 0.005277 0.037683
Perche 0.002598 0.033342 0.490490 0.311794 0.111595 0.042057 0.004438 0.012895 0.001516 0.014275
Javelot 0.078710 0.102994 0.155627 0.520022 0.095431 0.016361 0.005270 0.035676 0.013469 0.001439
1500m 0.003457 0.230510 0.627041 0.026598 0.024171 0.054646 0.003235 0.000077 0.020852 0.034413
In [19]:
cos2_pc12 = cos2['PC1']+cos2['PC2']
cos2_pc12
Out[19]:
100m        0.651093
Longueur    0.686474
Poids       0.764113
Hauteur     0.461073
400m        0.805782
110m H      0.624459
Disque      0.689656
Perche      0.035939
Javelot     0.181704
1500m       0.233968
dtype: float64
In [20]:
#Echelle de couleurs pour représenter les cos2
import cmasher as cmr
import matplotlib.colors as mcolors

cmap = cmr.get_sub_cmap('cmr.ember', 0.1, 0.9)
norm = mcolors.Normalize(vmin=0, vmax=1)
color=[]
for i in range(len(cos2_pc12)):
    color.append(cmap(norm(cos2_pc12[i])))
df_cos2=pd.DataFrame({'cos2':cos2_pc12, 'couleur':color})
df_cos2
Out[20]:
cos2 couleur
100m 0.651093 (0.8574156, 0.25216222, 0.09236499, 1.0)
Longueur 0.686474 (0.87617742, 0.29951158, 0.06895674, 1.0)
Poids 0.764113 (0.91066237, 0.40470882, 0.01887928, 1.0)
Hauteur 0.461073 (0.67755703, 0.037385, 0.23467744, 1.0)
400m 0.805782 (0.92411787, 0.45632484, 0.00609634, 1.0)
110m H 0.624459 (0.83912274, 0.21082034, 0.11285311, 1.0)
Disque 0.689656 (0.87865734, 0.30619915, 0.06561995, 1.0)
Perche 0.035939 (0.11611032, 0.06513579, 0.13799796, 1.0)
Javelot 0.181704 (0.29929909, 0.10148169, 0.23800843, 1.0)
1500m 0.233968 (0.37076004, 0.10304797, 0.2591165, 1.0)
In [21]:
# Représentation par un cercle de corrélation
fig, ax = plt.subplots(figsize=(7,7))
ax.set_aspect('equal')
# Tracer les axes
plt.axhline(y=0, color='black', linewidth=1, linestyle='--')
plt.axvline(x=0, color='black', linewidth=1, linestyle='--')

# Tracer le cercle de rayon 1
circle = plt.Circle((0,0),1, edgecolor='black', facecolor='white')
ax.add_artist(circle)

# Représentation des vecteurs
for i in range(saturations.shape[0]):
    x = saturations.iloc[i,0] # coord de la var i selon PC1
    y = saturations.iloc[i,1] # coord de la var i selon PC2
    feature = saturations.index[i] # nom de la var i
    
    ax.arrow(0,0,  #départ flèche
         x,y,  #fin flèche
         head_width=0.05,
         head_length=0.05,
         color=df_cos2.iloc[i,1],
         length_includes_head=True
        )
    if feature!='Poids':
        if x>0:
            ax.text(x+0.05, y+0.05, feature)
        else:
            ax.text(x-0.15, y+0.05, feature)
    else :
        ax.text(x+0.05,y-0.05, feature)

# Vecteur de classement
ax.arrow(0,0,  #départ flèche
         sat_class_pc1, sat_class_pc2,  #fin flèche
         head_width=0.05,
         head_length=0.05,
         color='tab:blue',
         length_includes_head=True
        )
ax.text(sat_class_pc1-0.35, sat_class_pc2, 'Classement', color='tab:blue')

# Vecteur de points
ax.arrow(0,0,  #départ flèche
         sat_pts_pc1, sat_pts_pc2,  #fin flèche
         head_width=0.05,
         head_length=0.05,
         color='tab:blue',
         length_includes_head=True
        )
ax.text(sat_pts_pc1-0.2, sat_pts_pc2-0.08, 'Points', color='tab:blue')

#legende des couleurs
sm = plt.cm.ScalarMappable(cmap=cmap)
sm.set_clim(vmin=0, vmax=1)
plt.colorbar(sm, label="cos2",  shrink=0.4,orientation='vertical',pad=0.1, ax=ax)
        
# labels et titres
plt.xlim(-1.2,1.2)
plt.ylim(-1.2,1.2)
plt.xticks([i for i in np.arange(-1,1.5,0.5)], [str(i) for i in np.arange(-1,1.5,0.5)])
plt.yticks([i for i in np.arange(-1,1.5,0.5)], [str(i) for i in np.arange(-1,1.5,0.5)])
plt.xlabel('PC1 ('+ str(round(pca.explained_variance_ratio_[0]*100,2)) + ' \%)')
plt.ylabel('PC2 ('+ str(round(pca.explained_variance_ratio_[1]*100,2)) + ' \%)')
plt.title('Cercle de corrélation des variables du jeu de données Decathlon')
plt.tight_layout()
plt.show()
  • Les variables les mieux représentées sur le plan factoriel (PC1, PC2) sont le 400m, le lancer du poids, le lancer du disque et le saut en longueur.

  • Appelons groupe 1 les disciplines suivantes : disque, poids. Appelons groupe 2 les disciplines suivantes : 400m, 100m, 110m haies, longueur.

    • Les variables du groupe 1 ont une bonne corrélation entre elles.
    • Les variables du groupe 2 aussi (corrélations parfois négatives).
    • En revanche les variables du groupe 1 sont très peu corrélées avec les variables du groupe 2.
  • Le nombre de points est corrélé positivement (mais pas très fortement) avec les performances au lancer du disque, au lancer du poids, au saut en longueur et au saut en hauteur.

  • Le nombre de points est corrélé négativement avec les performances aux courses de vitesse.

Cercle de corrélations selon PC3 et PC4¶

Certaines variables comme le javelot, le saut à la perche et le 1500m ne sont pas bien expliquées par le plan factoriel (PC1, PC2). D'après la table des cos2, le javelot et le saut à la perche sont mieux expliquées par le plan
(PC3, PC4) alors que le 1500m est mieux expliqué par le plan (PC2, PC3). Regardons le cercle de corrélation du plan (PC3, PC4).

In [22]:
# Calcul des cos2
cos2_pc34 = cos2['PC3']+cos2['PC4']
cos2_pc34
Out[22]:
100m        0.036322
Longueur    0.044650
Poids       0.037794
Hauteur     0.087876
400m        0.018596
110m H      0.095493
Disque      0.071006
Perche      0.802285
Javelot     0.675650
1500m       0.653639
dtype: float64
In [23]:
#Echelle de couleurs pour représenter les cos2
cmap = cmr.get_sub_cmap('cmr.ember', 0.1, 0.9)
norm = mcolors.Normalize(vmin=0, vmax=1)
color=[]
for i in range(len(cos2_pc34)):
    color.append(cmap(norm(cos2_pc34[i])))
df_cos2_34=pd.DataFrame({'cos2':cos2_pc34, 'couleur':color})
df_cos2_34
Out[23]:
cos2 couleur
100m 0.036322 (0.11611032, 0.06513579, 0.13799796, 1.0)
Longueur 0.044650 (0.12770344, 0.06903174, 0.14661238, 1.0)
Poids 0.037794 (0.11611032, 0.06513579, 0.13799796, 1.0)
Hauteur 0.087876 (0.18105903, 0.08385552, 0.18180624, 1.0)
400m 0.018596 (0.09324732, 0.05663497, 0.11982592, 1.0)
110m H 0.095493 (0.18709992, 0.08524014, 0.18537066, 1.0)
Disque 0.071006 (0.15711534, 0.07779942, 0.16686569, 1.0)
Perche 0.802285 (0.92411787, 0.45632484, 0.00609634, 1.0)
Javelot 0.675650 (0.87107311, 0.286081, 0.07562583, 1.0)
1500m 0.653639 (0.8574156, 0.25216222, 0.09236499, 1.0)
In [24]:
# Représentation par un cercle de corrélation
fig, ax = plt.subplots(figsize=(7,7))
ax.set_aspect('equal')
# Tracer les axes
plt.axhline(y=0, color='black', linewidth=1, linestyle='--')
plt.axvline(x=0, color='black', linewidth=1, linestyle='--')

# Tracer le cercle de rayon 1
circle = plt.Circle((0,0),1, edgecolor='black', facecolor='white')
ax.add_artist(circle)

# Représentation des vecteurs
for i in range(saturations.shape[0]):
    x = saturations.iloc[i,2] # coord de la var i selon PC3
    y = saturations.iloc[i,3] # coord de la var i selon PC4
    feature = saturations.index[i] # nom de la var i
    
    ax.arrow(0,0,  #départ flèche
         x,y,  #fin flèche
         head_width=0.05,
         head_length=0.05,
         color=df_cos2_34.iloc[i,1],
         length_includes_head=True
        )
    if feature!='':
        if x>0:
            plt.text(x+0.03, y+0.03, feature)
        else:
            plt.text(x-0.2, y, feature)
    else :
        plt.text(x+0.05,y-0.05, feature)
        
# Vecteur de classement
ax.arrow(0,0,  #départ flèche
         sat_class_pc3, sat_class_pc4,  #fin flèche
         head_width=0.05,
         head_length=0.05,
         color='tab:blue',
         length_includes_head=True
        )
ax.text(sat_class_pc3-0.1, sat_class_pc4+0.06, 'Classement', color='tab:blue')

# Vecteur de points
ax.arrow(0,0,  #départ flèche
         sat_pts_pc3, sat_pts_pc4,  #fin flèche
         head_width=0.05,
         head_length=0.05,
         color='tab:blue',
         length_includes_head=True
        )
ax.text(sat_pts_pc3-0.2, sat_pts_pc4-0.08, 'Points', color='tab:blue')

#legende des couleurs
sm = plt.cm.ScalarMappable(cmap=cmap)
sm.set_clim(vmin=0, vmax=1)
plt.colorbar(sm, label="cos2",  shrink=0.4,orientation='vertical',pad=0.1, ax=ax)
        
# labels et titres
plt.xlim(-1.2,1.2)
plt.ylim(-1.2,1.2)
plt.xticks([i for i in np.arange(-1,1.5,0.5)], [str(i) for i in np.arange(-1,1.5,0.5)])
plt.yticks([i for i in np.arange(-1,1.5,0.5)], [str(i) for i in np.arange(-1,1.5,0.5)])
plt.xlabel('PC3 ('+ str(round(pca.explained_variance_ratio_[2]*100,2)) + ' \%)')
plt.ylabel('PC4 ('+ str(round(pca.explained_variance_ratio_[3]*100,2)) + ' \%)')
plt.title('Cercle de corrélation des variables du jeu de données Decathlon')
plt.tight_layout()
plt.show()

Dans ce plan, les variables Perche et javelot sont bien mieux représentées. Le 1500 m également puisque cette variable est expliquée en bonne partie par PC3.

En revanche les autres disciplines sont très mal représentées sur ce plan.

Finalement, la seule chose qu'apporte ce plan est que les performances au saut à la perche ne sont quasiment pas corrélées aux performances au javelot.


2. Biplot¶

In [25]:
cos2_indiv = ((features_trans**2)
              .divide(((features_trans**2)
                       .sum(axis=1)),axis=0)
             )
cos2_indiv.head()
Out[25]:
PC1 PC2 PC3 PC4 PC5 PC6 PC7 PC8 PC9 PC10
0 0.695410 0.079543 0.003585 0.160666 0.006059 0.000196 0.013133 0.024151 0.000165 0.017092
1 0.711205 0.032432 0.002474 0.103335 0.049846 0.030575 0.034843 0.004246 0.000008 0.031034
2 0.851755 0.000064 0.000069 0.068851 0.001406 0.021948 0.008230 0.045760 0.001297 0.000619
3 0.423049 0.092039 0.294777 0.046845 0.080996 0.000136 0.003149 0.040347 0.016301 0.002360
4 0.529944 0.366472 0.081626 0.009129 0.002583 0.000710 0.000419 0.005188 0.003158 0.000771
In [26]:
cos2_indiv_12 = cos2_indiv['PC1'] + cos2_indiv['PC2']
In [27]:
import seaborn as sns
import cmasher as cmr

# Représentation des individus suivant les deux premières composantes
fig, ax = plt.subplots(figsize=(6,6))
plt.axhline(y=0, color='black', linewidth=1)
plt.axvline(x=0, color='black', linewidth=1)
sns.scatterplot(x='PC1', y='PC2', data=features_trans, size=cos2_indiv_12)
ax.set_xlabel('PC1 ('+ str(round(pca.explained_variance_ratio_[0]*100,2)) + ' \%)')
ax.set_ylabel('PC2 ('+ str(round(pca.explained_variance_ratio_[1]*100,2)) + ' \%)')
plt.xlim(-5,5)
plt.ylim(-5,5)

#deuxième repère pour les vecteurs
ax2 = fig.add_axes(ax.get_position(), frame_on=False)
ax2.xaxis.tick_top()
ax2.yaxis.tick_right()
ax2.xaxis.set_label_position('top') 
ax2.yaxis.set_label_position('right') 
ax2.set_xlabel('Variables initiales selon PC1', color='chocolate')
ax2.set_ylabel('Variables initiales selon PC2', color='chocolate')
ax2.tick_params(axis='both', labelcolor='chocolate')
ax2.set_xlim(-1.1, 1.1)
ax2.set_ylim(-1.1, 1.1)
ax2.set_xticks([-1, -0.5, 0, 0.5, 1])
ax2.set_yticks([-1, -0.5, 0, 0.5, 1])

# Représentation des vecteurs
for i in range(saturations.shape[0]):
    x = saturations.iloc[i,0] # coord de la var i selon PC1
    y = saturations.iloc[i,1] # coord de la var i selon PC2
    feature = saturations.index[i] # nom de la var i
    
    ax2.arrow(0,0,  #départ flèche
         x,y,  #fin flèche
         head_width=0.025,
         head_length=0.025,
         length_includes_head=True,
              color='chocolate'
        )
    if feature!='Poids':
        if x>0:
            plt.text(x+0.05, y+0.05, feature)
        else:
            plt.text(x-0.15, y+0.05, feature)
    else :
        plt.text(x+0.05,y-0.05, feature)
    


# Titre
plt.title("Biplot : athlètes et disciplines suivant les deux premières composantes de l'ACP")
ax.legend(title='Cos2 des individus', bbox_to_anchor=[1.4,1])

# Grille
ax.grid(visible=True)
ax.set_axisbelow(True)  # grille en arrière-plan
plt.show()

Les individus les mieux représentés sont les plus éloignés de l'origine.


3. Représentation des individus suivant les trois premières composantes principales.¶

In [28]:
cos2_indiv_123 = cos2_indiv['PC1'] + cos2_indiv['PC2'] + cos2_indiv['PC3']
In [29]:
#Echelle de couleurs pour représenter les cos2
cmap = cmr.get_sub_cmap('cmr.ember', 0.1, 0.9)
norm = mcolors.Normalize(vmin=0, vmax=1)
color=[]
for i in range(len(cos2_indiv_123)):
    color.append(cmap(norm(cos2_indiv_123[i])))
df_cos2_123=pd.DataFrame({'cos2':cos2_indiv_123, 'couleur':color})
df_cos2_123.head()
Out[29]:
cos2 couleur
0 0.778538 (0.91598827, 0.42411283, 0.01281748, 1.0)
1 0.746111 (0.90302556, 0.3787143, 0.02922685, 1.0)
2 0.851888 (0.93763131, 0.5204957, 0.00678164, 1.0)
3 0.809866 (0.92563324, 0.46275282, 0.00528364, 1.0)
4 0.978041 (0.95551873, 0.68816819, 0.10107749, 1.0)
In [30]:
# Création de la figure
fig = plt.figure(figsize = (30, 10))
ax = plt.axes(projection ="3d")

# Création du graphique
sctt = ax.scatter3D(features_trans['PC1'], features_trans['PC2'], features_trans['PC3'], c=df_cos2_123['couleur'])

# Titres et labels
ax.set_xlabel('PC1 ('+ str(round(pca.explained_variance_ratio_[0]*100,2)) + ' \%)')
ax.set_ylabel('PC2 ('+ str(round(pca.explained_variance_ratio_[1]*100,2)) + ' \%)')
ax.set_zlabel('PC3 ('+ str(round(pca.explained_variance_ratio_[2]*100,2)) + ' \%)')
ax.set_title("Répartition des décathlètes suivant les trois premières composantes principales")

#legende des couleurs
sm = plt.cm.ScalarMappable(cmap=cmap)
sm.set_clim(vmin=0, vmax=1)
plt.colorbar(sm, label="Cos2", shrink=0.3, orientation='vertical',pad=0.1, anchor = (0.5,0.5), ax=ax)

# Ajustement de l'affichage
ax.tick_params(axis='x', pad=-0.5)
ax.tick_params(axis='y', pad=-0.5)
ax.tick_params(axis='z', pad=-0.5)
plt.subplots_adjust(left=0.6, right=0.77, top=1, bottom=0.2)
plt.show()

Difficile de distinguer la forme du nuage de points. Utilisons une version interactive.

In [31]:
import plotly.express as px


# Création du graphe
fig = px.scatter_3d(features_trans, x='PC1', y='PC2', z='PC3',
                   color=cos2_indiv_123, color_continuous_scale=['black', 'firebrick', 'orange'])

# Changement fenêtre de survol
fig.update_traces(hovertemplate="PC1: %{x}<br>PC2: %{y}<br>PC3: %{z}<br>Cos2: %{customdata[0]}",
                  customdata=np.round(df_cos2_123[['cos2']].values,2))

# Ajout d'une bordure aux points
fig.update_traces(marker=dict(
                            size=4,
                            line=dict(width=1,
                                      color='DarkSlateGrey')
                            )
                 )

# titres, labels et vue d'origine
fig.update_layout(width=1000,
                  height=800,
                  scene = dict(
                            xaxis_title='PC1 ('+ str(round(pca.explained_variance_ratio_[0]*100,2)) + ' %)',
                            yaxis_title='PC2 ('+ str(round(pca.explained_variance_ratio_[1]*100,2)) + ' %)',
                            zaxis_title='PC3 ('+ str(round(pca.explained_variance_ratio_[2]*100,2)) + ' %)'),
                  title_text="Répartition des décathlètes suivant les trois premières composantes principales",
                  title_y=0.9,
                  scene_camera=dict(
                      eye=dict(x=1.8, y=-1.25, z=0.8)),
                  autosize=False,
                  coloraxis_colorbar_title ="Cos2",
                  )

#Affichage
fig.show()

4. Nommage des cinq premières composantes¶

Calcul des contributions de chaque variable à chaque composante.

In [32]:
contributions = round(cos2/(cos2.sum(axis=0))*100,2)
contributions
Out[32]:
PC1 PC2 PC3 PC4 PC5 PC6 PC7 PC8 PC9 PC10
100m 18.34 2.02 2.42 0.14 13.34 8.77 14.58 21.31 1.10 18.00
Longueur 16.82 6.87 2.36 0.98 0.20 9.37 39.40 0.04 23.30 0.66
Poids 11.84 20.61 0.04 3.44 1.80 9.33 9.59 9.86 18.26 15.23
Hauteur 10.00 7.06 4.79 1.74 45.05 21.88 0.84 1.56 5.94 1.13
400m 14.12 18.67 1.23 0.08 1.12 11.06 1.55 4.55 30.48 17.14
110m H 17.02 3.01 0.61 8.00 3.94 0.99 12.77 50.57 2.25 0.83
Disque 9.33 21.16 0.13 6.38 1.60 20.19 18.48 0.15 2.40 20.17
Perche 0.08 1.87 34.06 28.78 15.90 6.85 0.96 3.17 0.69 7.64
Javelot 2.35 5.78 10.81 48.00 13.60 2.66 1.14 8.77 6.12 0.77
1500m 0.10 12.95 43.54 2.46 3.44 8.90 0.70 0.02 9.47 18.42

Pour chacune des cinq premières composantes, regardons les variables qui y contribuent le plus.

In [33]:
for col in contributions.columns[0:5]:
    df = contributions.loc[:,col].sort_values(ascending=False).reset_index()
    fig, ax = plt.subplots(figsize=(8,2))
    sns.barplot(data=df, x='index', y=col, color="tab:blue")
    plt.title("Contributions des variables à " +col)
    plt.xlabel("Variable")
    plt.ylabel("Contribution (\%)")
    plt.show()

Il est difficile de nommer les cinq premières composantes. Des disciplines très différentes contribuent fortement à chacune d'entre-elles.


5. Avantages et inconvénients des méthodes KMeans et CAH.¶

KMeans¶

Avantages¶

  • L'algorithme KMeans est facile d'utilisation
  • Il est rapide et est adapté à une grande quantité de données.
  • Il est facile à comprendre.
  • Lors de l'ajout de nouvelles données, on peut leur attribuer un cluster (celui du centroïde dont elles sont le plus proche) sans avoir à relancer une analyse de clustering.

Inconvénients¶

  • L'utilisateur doit définir le nombre final de clusters avant de lancer l'algorithme. Si cela peut-être un avantage si on sait combien de clusters on veut former, c'est un inconvénient dans les autres cas. Si l'on souhaite déterminer le nombre optimal de clusters, il faut alors exécuter l'algorithme pour les différentes valeurs k de clusters puis mener une analyse pour faire le choix optimal.
  • Il n'est pas adapté à des formes de clusters complexes.
  • Il est sensible aux outliers.
  • Il est sensible à la position initiale des centroïdes. Il convient donc de l'exécuter plusieurs fois avec des positions initiales différentes et de choisir le résultat le plus optimal.

Classification Ascendante Hiérarchique¶

Avantages¶

  • Il n'est pas nécessaire de définir le nombre de clusters. Ce choix se fait après l'exécution de l'algorithme en fonction des résultats obtenus.
  • La visualisation sous forme de dendogramme permet une bonne compréhension du regroupement progressif des données.
  • On peut choisir le type de distance utilisé pour calculer les distances entre individus ou groupes d'individus.

Inconvénients¶

  • Mauvaise complexité algorithmique : le temps de calcul devient vite trop important lorsque le nombre de données augmente.
  • L'ajout de nouvelles données nécesite de relancer complètement le clustering.

6. Clustering¶

6.1 KMeans¶

In [34]:
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score

dico_inert = dict()
dico_silh = dict()

data = features_trans.iloc[:,0:5] #uniquement les 4 premières PC

for k in range(1,11):
    kmeans = KMeans(n_clusters=k, n_init=100)
    kmeans.fit(data)
    clusters = kmeans.predict(data)
    dico_inert[k] = kmeans.inertia_
    if k>1:
        dico_silh[k] = silhouette_score(data, clusters)
In [35]:
sq_sum = pd.DataFrame(list(dico_inert.items()), columns=['n_clusters', 'inertie'])
sq_sum
Out[35]:
n_clusters inertie
0 1 334.378662
1 2 253.464351
2 3 205.075709
3 4 175.858653
4 5 149.589562
5 6 127.511105
6 7 114.867138
7 8 102.721672
8 9 93.350658
9 10 82.488584
In [36]:
# Représentation de la variance intra-clusters suivant le nombre de clusters
fig, ax = plt.subplots(figsize=(8,5))

plt.plot(sq_sum['n_clusters'], sq_sum['inertie'], color='tab:blue', marker='x')
plt.xticks(range(1,11), range(1,11))
plt.xlabel('Nombre de clusters')
plt.ylabel('Somme des carrés intra-cluster')
plt.title('Somme des carrés intra-cluster en fonction du nombre de clusters')
plt.tight_layout()
plt.show()

Il n'y a pas de coude franc.

In [37]:
df_sil = pd.DataFrame(list(dico_silh.items()), columns=['n_clusters', 'silhouette'])
df_sil 
Out[37]:
n_clusters silhouette
0 2 0.200473
1 3 0.185363
2 4 0.198417
3 5 0.222477
4 6 0.247857
5 7 0.231854
6 8 0.230253
7 9 0.226142
8 10 0.234294
In [38]:
fig, ax = plt.subplots(figsize=(10,5))

sns.lineplot(data=df_sil, x='n_clusters', y='silhouette')
sns.scatterplot(data=df_sil, x='n_clusters', y='silhouette')
plt.title('Score de silhouette en fonction du nombre de clusters')
plt.xlabel('Nombre de clusters')
plt.ylabel('Score de silhouette')

plt.show()

On peut choisir 6 clusters (meilleur score de silhouette).

In [39]:
kmeans = KMeans(n_clusters=6, n_init='auto')
kmeans.fit(data)
clusters = kmeans.predict(data)
In [40]:
df_clusters = pd.concat([data, pd.DataFrame({'Cluster':clusters})], axis=1)
df_clusters.head()
Out[40]:
PC1 PC2 PC3 PC4 PC5 Cluster
0 4.038449 1.365826 -0.289957 1.941134 -0.376955 4
1 3.919365 0.836961 0.231175 1.493972 1.037609 4
2 4.619987 0.039995 -0.041586 -1.313526 -0.187730 4
3 2.233461 1.041766 -1.864362 -0.743214 -0.977270 4
4 2.168396 -1.803200 0.851017 -0.284600 0.151395 0
In [41]:
# Représentation des individus suivant PC1 et PC3
fig, ax = plt.subplots(figsize=(7,7))
plt.axhline(y=0, color='black', linewidth=1)
plt.axvline(x=0, color='black', linewidth=1)
sns.scatterplot(x='PC1', y='PC2', data=df_clusters, hue='Cluster', palette='tab10')
ax.set_xlabel('PC1 ('+ str(round(pca.explained_variance_ratio_[0]*100,2)) + ' \%)')
ax.set_ylabel('PC2 ('+ str(round(pca.explained_variance_ratio_[2]*100,2)) + ' \%)')
plt.xlim(-5,5)
plt.ylim(-5,5)

#deuxième repère pour les vecteurs
ax2 = fig.add_axes(ax.get_position(), frame_on=False)
ax2.xaxis.tick_top()
ax2.yaxis.tick_right()
ax2.xaxis.set_label_position('top') 
ax2.yaxis.set_label_position('right') 
ax2.set_xlabel('Variables initiales selon PC1', color='darkorange')
ax2.set_ylabel('Variables initiales selon PC2', color='darkorange')
ax2.tick_params(axis='both', labelcolor='darkorange')
ax2.set_xlim(-1,1)
ax2.set_ylim(-1,1)
ax2.set_xticks([-1, -0.5, 0, 0.5, 1])
ax2.set_yticks([-1, -0.5, 0, 0.5, 1])

# Représentation des vecteurs
for i in range(saturations.shape[0]):
    x = saturations.iloc[i,0] # coord de la var i selon PC1
    y = saturations.iloc[i,1] # coord de la var i selon PC2
    feature = saturations.index[i] # nom de la var i
    
    ax2.arrow(0,0,  #départ flèche
         x,y,  #fin flèche
         head_width=0.025,
         head_length=0.025,
         length_includes_head=True,
              color='darkorange'
        )
    if feature!='Poids':
        if x>0:
            plt.text(x+0.03, y+0.05, feature)
        else:
            plt.text(x-0.15, y+0.05, feature)
    else :
        plt.text(x+0.05,y-0.05, feature)
    


# Titre
plt.title("Biplot : athlètes et disciplines suivant les deux premières composantes de l'ACP")
ax.legend(title='Cluster', bbox_to_anchor=[1.35,1])

# Grille
ax.grid(visible=True)
ax.set_axisbelow(True)  # grille en arrière-plan
plt.show()

Sur le plan (PC1, PC2), les clusters ne sont pas tous clairement séparés. Mais n'oublions pas que les clustering n'a pas été fait que sur les deux première composantes principales.

6.2 Classification Ascendante Hierarchique¶

In [42]:
from scipy.cluster.hierarchy import linkage, dendrogram

mergings = linkage(data, method='complete')

noms = df_decathlon['Nom'].to_list()

fig, ax = plt.subplots(figsize=(8,6))
dendrogram(mergings, labels=noms , leaf_rotation=90, leaf_font_size=7)
plt.axhline(4.5, color='black', linestyle='--')
plt.show()

D'après le dendrogramme, le choix de 6 clusters paraît être la meilleure solution. La ligne noire en pointillés indique à quel endroit se fait le "découpage". On remarque que les distances entre les différents groupes est assez grande à cette hauteur, ce qui n'était pas le cas à une hauteur plus faible. Cela indique que les clusters formés sont assez différents les uns des autres.

Une hautre méthode est de prendre une hauteur égale à 75 % de la hauteur maximale pour faire le découpage en clusters.

In [43]:
seuil = 0.75 * max(mergings[:, 2])

fig, ax = plt.subplots(figsize=(8,6))
dendrogram(mergings, labels=noms , leaf_rotation=90, leaf_font_size=7)
plt.axhline(seuil, color='black', linestyle='--')
plt.show()

Cette méthode suggère de ne conserver que 3 clusters. Dans la suite, je garderai 6 clusters.

In [44]:
from scipy.cluster.hierarchy import fcluster

clusters_cah = fcluster(mergings, t=4.5, criterion='distance')
df_clusters_cah = pd.concat([data, pd.DataFrame({'Cluster':clusters_cah})], axis=1)
df_clusters_cah.head()
Out[44]:
PC1 PC2 PC3 PC4 PC5 Cluster
0 4.038449 1.365826 -0.289957 1.941134 -0.376955 5
1 3.919365 0.836961 0.231175 1.493972 1.037609 5
2 4.619987 0.039995 -0.041586 -1.313526 -0.187730 5
3 2.233461 1.041766 -1.864362 -0.743214 -0.977270 6
4 2.168396 -1.803200 0.851017 -0.284600 0.151395 3
In [45]:
# Représentation des individus suivant PC1 et PC3
fig, ax = plt.subplots(figsize=(7,7))
plt.axhline(y=0, color='black', linewidth=1)
plt.axvline(x=0, color='black', linewidth=1)
sns.scatterplot(x='PC1', y='PC2', data=df_clusters_cah, hue='Cluster', palette='tab10')
ax.set_xlabel('PC1 ('+ str(round(pca.explained_variance_ratio_[0]*100,2)) + ' \%)')
ax.set_ylabel('PC2 ('+ str(round(pca.explained_variance_ratio_[2]*100,2)) + ' \%)')
plt.xlim(-5,5)
plt.ylim(-5,5)

#deuxième repère pour les vecteurs
ax2 = fig.add_axes(ax.get_position(), frame_on=False)
ax2.xaxis.tick_top()
ax2.yaxis.tick_right()
ax2.xaxis.set_label_position('top') 
ax2.yaxis.set_label_position('right') 
ax2.set_xlabel('Variables initiales selon PC1', color='darkorange')
ax2.set_ylabel('Variables initiales selon PC2', color='darkorange')
ax2.tick_params(axis='both', labelcolor='darkorange')
ax2.set_xlim(-1,1)
ax2.set_ylim(-1,1)
ax2.set_xticks([-1, -0.5, 0, 0.5, 1])
ax2.set_yticks([-1, -0.5, 0, 0.5, 1])

# Représentation des vecteurs
for i in range(saturations.shape[0]):
    x = saturations.iloc[i,0] # coord de la var i selon PC1
    y = saturations.iloc[i,1] # coord de la var i selon PC2
    feature = saturations.index[i] # nom de la var i
    
    ax2.arrow(0,0,  #départ flèche
         x,y,  #fin flèche
         head_width=0.025,
         head_length=0.025,
         length_includes_head=True,
              color='darkorange'
        )
    if feature!='Poids':
        if x>0:
            plt.text(x+0.03, y+0.05, feature)
        else:
            plt.text(x-0.15, y+0.05, feature)
    else :
        plt.text(x+0.05,y-0.05, feature)
    


# Titre
plt.title("Biplot : athlètes et disciplines suivant les deux premières composantes de l'ACP")
ax.legend(title='Cluster', bbox_to_anchor=[1.35,1])

# Grille
ax.grid(visible=True)
ax.set_axisbelow(True)  # grille en arrière-plan
plt.show()

7. Dendrogramme¶

fait dans la partie 6


8. Dendrogramme en 3D¶

In [46]:
import plotly.graph_objects as go

fig = go.Figure()

# ensemble des clusters
unique_clusters = df_clusters_cah['Cluster'].sort_values().unique()
# Couleurs des clusters
colors = ['#053BA9', '#0A8D02', '#F7B200', '#CD0808', '#7810CE', '#10B7CE']

# couleur des points
conditions = [df_clusters_cah['Cluster'] ==1, df_clusters_cah['Cluster'] ==2, df_clusters_cah['Cluster'] ==3,
              df_clusters_cah['Cluster'] ==4, df_clusters_cah['Cluster'] ==5, df_clusters_cah['Cluster'] ==6]
df_clusters_cah['couleur'] =np.select(conditions, colors)

# dico des coordonnées des points à placer (
# au départ, individus du jeu de données

points = {i:(df_clusters_cah.iloc[i,0], df_clusters_cah.iloc[i,1], 0, df_clusters_cah.iloc[i,6]) for i in range(41)}
points


# Sélection et représentation des individus de chaque cluster
for i, cluster in enumerate(unique_clusters):
    df = df_clusters_cah[df_clusters_cah['Cluster'] == cluster]
    fig.add_trace(go.Scatter3d(x=df['PC1'], y=df['PC2'], z=[0 for i in range(len(df))],
                               mode='markers',
                               marker=dict(size=6,
                                           color=colors[i],    # définir la couleur des points en fonction de la variable colors
                                           opacity=1,
                                           line=dict(width=1,
                                                      color='DarkSlateGrey')),
                               name=f'{cluster}'
                              )
                 )

der_pt = 40     #index du dernier point
# Pour chaque regroupement de mergings
for group in mergings:
    pt1 = group[0]
    pt2 = group[1]
    ht = group[2]
    #calcul des coordonnées du point de jonction et incrémentation de der_pt
    der_pt = der_pt+1
    x = (points[pt1][0] + points[pt2][0])/2
    y = (points[pt1][1] + points[pt2][1])/2
    
    # si les deux points d'origine ont même couleur
    # alors le point et les lignes de jonction ont la même couleur
    # sinon, ils sont noir
    if points[pt1][3] == points[pt2][3]:
        c = points[pt1][3]
    else:
        c = 'black'
        
    #on place le point de jonction dans liste des points
    points[der_pt] = (x, y, ht, c)
            
    #lignes de regroupement
    # Si les points d'origine ont même couleur, alors les lignes ont même couleur
    # 1ere ligne vert du regroupement
    
    fig.add_trace(go.Scatter3d(x=[points[pt1][0],points[pt1][0]], y=[points[pt1][1],points[pt1][1]], z=[points[pt1][2],ht],
                               mode='lines',
                               line=dict(color=c,
                                         width=2),
                               showlegend=False
                              )
                 )
    # 2eme ligne vert du regroupement    
    fig.add_trace(go.Scatter3d(x=[points[pt2][0],points[pt2][0]], y=[points[pt2][1],points[pt2][1]], z=[points[pt2][2],ht],
                               mode='lines',
                               line=dict(color=c,
                                         width=2),
                               showlegend=False
                              )
                 )
    # ligne horiz du regroupement    
    fig.add_trace(go.Scatter3d(x=[points[pt1][0],points[pt2][0]], y=[points[pt1][1],points[pt2][1]], z=[ht,ht],
                               mode='lines',
                               line=dict(color=c,
                                         width=2),
                               showlegend=False
                              )
                 )

fig.update_layout(width=1000,
                 height=800,
                 scene=dict(xaxis=dict(showbackground=False, gridcolor='rgba(100, 150, 255, 0.8)', zerolinecolor='rgba(0, 0, 0, 1)'),
                            yaxis=dict(showbackground=False, gridcolor='rgba(100, 150, 255, 0.8)', zerolinecolor='rgba(0, 0, 0, 1)'),
                            zaxis=dict(showbackground=False, gridcolor='rgba(100, 150, 255, 0.8)', zerolinecolor='rgba(0, 0, 0, 1)',range=[0, max(mergings[:,2])]),
                            xaxis_title='PC1 ('+ str(round(pca.explained_variance_ratio_[0]*100,2)) + ' %)',
                            yaxis_title='PC2 ('+ str(round(pca.explained_variance_ratio_[1]*100,2)) + ' %)',
                            zaxis_title='Distance'),
                 legend=dict(title='Cluster'),
                 title='Classification Ascendante Hiérarchique des athlètes de décathlon'
                )
fig.show()
In [ ]: